@spinabot/brigade 0.1.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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +154 -0
- package/SECURITY.md +208 -0
- package/assets/brigade-wordmark-on-black.png +0 -0
- package/assets/brigade-wordmark.png +0 -0
- package/brigade.mjs +96 -0
- package/dist/cli/chat-cmd.js +120 -0
- package/dist/cli/config-cmd.js +132 -0
- package/dist/cli/connect-cmd.js +447 -0
- package/dist/cli/doctor-cmd.js +317 -0
- package/dist/cli/gateway-cmd.js +92 -0
- package/dist/cli.js +287 -0
- package/dist/core/agent.js +1123 -0
- package/dist/core/config.js +80 -0
- package/dist/core/console-stream.js +188 -0
- package/dist/core/error-classifier.js +354 -0
- package/dist/core/event-logger.js +122 -0
- package/dist/core/model-caps.js +185 -0
- package/dist/core/provider-payload-mutators.js +517 -0
- package/dist/core/provider-quirks.js +285 -0
- package/dist/core/server.js +459 -0
- package/dist/core/smart-compaction.js +209 -0
- package/dist/core/system-prompt-defaults.js +88 -0
- package/dist/core/system-prompt-guidance.js +269 -0
- package/dist/core/system-prompt.js +884 -0
- package/dist/index.js +30 -0
- package/dist/integrations/ollama.js +140 -0
- package/dist/protocol.js +49 -0
- package/dist/providers/catalog.js +100 -0
- package/dist/providers/validate-key.js +197 -0
- package/dist/tui/client.js +263 -0
- package/dist/ui/brand-frames-cli.js +20 -0
- package/dist/ui/brand-frames.js +36 -0
- package/dist/ui/brand.js +402 -0
- package/dist/ui/chat.js +929 -0
- package/dist/ui/onboarding.js +400 -0
- package/dist/ui/theme.js +51 -0
- package/package.json +92 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Auto-generated by scripts/build-brand-frames.mjs from the source MP4.
|
|
2
|
+
// Each frame is 16×16 source pixels (RGB24, base64-encoded).
|
|
3
|
+
// Rendered as 16 cols × 8 terminal rows via half-block (▀) cells.
|
|
4
|
+
// Re-run the build script if the source video changes.
|
|
5
|
+
export const VIDEO_COLS = 16;
|
|
6
|
+
export const VIDEO_SRC_ROWS = 16;
|
|
7
|
+
export const VIDEO_TERM_ROWS = 8;
|
|
8
|
+
export const VIDEO_FRAME_COUNT = 24;
|
|
9
|
+
export const VIDEO_FPS = 4;
|
|
10
|
+
export const VIDEO_FRAMES_BASE64 = [
|
|
11
|
+
"AQACAAABAQACAAAAAAAAAAAAAAAAdXBZhH5iBQAAAAAAAAAAAAAAAQACAAABAAABAAABAQACAAAACwkKcXSPODtWPTAA/O+y//evVEMAKSxGdHaRFBITAAAAAQAAAAAAAAAAAgABAAAAPDhB//7/oqDRFBcaWVtfYmVkFBcWi4q8////U1BdAAAAAwECAAAAAAAAAwECAAABBAQJYnfAhJnhg6LrkbD5j6/0hKXpgZngcInQDw0XAAABAwECAAAABQIAAAAAAAAtWmuZssj63/X/7/T55enu6Onv7/D24PX/u9D8ZXelAAY0AAAABAEAAAAAHx8jVmmbxNj/zMzReHh9UlBRTUtMT0xNUlBRdXV3xcXIy9//W2+hICEnAAAAPFB0yt7/0eD2nq3DX1kvw72TdnFmPTgtPDcsaGRYxsGXbGY8l6S7zNnwyN3/SV6Gi6vtr8//xM/aU15pi4hdenZLjIhzVlE8TUk0j4p1d3FIj4lfTFZewMnRrMv/i6v0ZIPPq8r/yNHpfYafTUgemJJocF4vn4xepY9hblgqlI9lUkwiZm+DydLmqMn/bY3bBBRBbn6rkaTP5ff/iouRQ0RKNigTcGJNfmhUPigUQ0JJfHuC4fP/m63Ud4u7DyJSAAAAAAAAAwYTb3N/8vf/5ev0ycrSs7S8sLW8xMnQ3+Tw8/f/hImZCQ0eAQAAAAAAAQACAQACAAAAAAAAUWuop8H+xNr/zeT/0OT/yNv/qcT/XXe3AAAAAAAAAQAAAQAAAAABAAABAAAEBAQJHCpMHCpMLkV7NUyCNk2DLkV7GShKGilLBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
12
|
+
"AQACAAABAAABAAABAQACAAAAAAAAAAAAAAAAUk8/SkU6AAAAAwAIAwAIAwACAgABAAABAAABAAAAAgABAAAALiwvvsDhUVJ0PTQA//m6//etLyEAAAAAAAABAAAAAAAAAAABAAABAAAABAQEAAAAQz5E4eX/gYTEIB4IhYNtfndeLygPhIamjZCvAgABAAAAAAADAQEFAQAAAAAAAhI+QVF9bpHnf6L5g6jycJbfV3O5NlKYvr3yy8r+EhAWAAAAAwAAAAAAAAISKC09VHfNncD/3+z/5PD/7fL+8PX/5/3/0uj/mK7rL0WCAAAAAwECAAAIMztKp8PztND/tsThw9HujYuCa2hfWVdYbWtsioiLtbO22ez/p7rlERguAAALAA04ip3IwN3/nrv1zc/ITE1Gta5lycJ4TUlKOzY3ODMiWVNDdnx82N7egY2pAAklAAs9fpLEq8b/qcX/oqCIWFY94d6oXVkjUE1dOzhIc20m7eegsq6SbWlN0+X/pbbbAAANLDNIhqHrnbf/3d3fPDw+hH1XYls1dGE8n4xniYE72NCKRkQ3PTsus8Puucn0AQAAAAAABhAlOkNY5ez/zNPoVlZaLCwwd1supYlbOi0WbWBJODQodG9kz+b/lq3mAAABAQEDAAAAAAAAMztK2ODv9Pn/w8jRnZqbhoSFdXN4eHZ8tLnJ2+Dwg5bDMURxAAABAAABAgEAAAAAGDd6X37Bm7f7wNv/4O7/5PL/5/X/4e//qLDAHyc2AAAAAAAAAAABAAABAQACAAAAEBQhEhcjHjFhOk19Ql6fVHCyS2adPFeNBQoTAAAAAwECAgABAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQACAgADAAABAAABAAABAAABAAABAAABAQACAQACAQEFAQEFAgADAQACAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
13
|
+
"AQACAAABAAABAAABAAABAAABAQACAAAAAAAAAAAAAAAACwYIAQAEAAABAQAEAAADAAABAAABAAABAAABAAAAAQEBAAAAIB4jt7nTUVJtLScA3titraR6BQAAAQAJAAAIAAABAAABAAACAQEDAwIAAAAAAAAAQj9F5+n/honMRTYA5NSU2MmMHg8AAAAAAAAGAAABAAACAAAAAAAAAAAADA0YOlirWHbJcZTvbZDrWXGiVG2eIipIV198ubbJJyU3AQAAAAAAAAUXUFlrf5rPhJ/TfJritdP/3uj96fT/3/b/xNv/iaHsfJTf4t//UU11AgADAAAAP1qYr8r/yuf/pMHw3+f0r7fEi4mKfXt8mpmYysnI6fn/0uH/Ym+WAAAjAAAFAAACTW2vrs3/k6zdxt//m5mBVFI6z8aBlo1INTAxQj0+WFlkt7nD0N7/FCJEAQAAAAAAGC9opbz1jaXe2fD/VE0ZubJ9vbuLb209RUNSOzhIYFk3dW5MtLzJd3+MAgAAAwAAAAAPLDNIdYm75/v/i4h1U1A9mZJwRD0bfHBXXVE33tZ/4tqEcHF3xsjNAAABAAABAQIAAAAAEhksytHk8vb/bnODMSMWXU9BxqhpfV8g0c2YZGAqMzZGur3NAAABAAABAAAAAwIBAAAXS1Rv0dr18Pr/ubOuc21oeGlbOyweRkEwPzopd4Srr7ziAAADAAADAwAAAAAACyNWT2eZZ4vfmLz/3un/6fX/0tbpvcHUr7PBxMjWk6XMM0RsAAABAAABAAABAAAABAMCBAMCDBs9OEhpSWWmZoLDipvBlqfMipGkO0JUAAAAAAAAAAABAAABAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQACAgADAAABAAABAAABAAABAAABAAABAAACAQEDAQACAQACAgABAgABAwEEAgADAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
14
|
+
"AQACAAABAAABAAABAAABAAABAQACAAABAAAAAAAAAAAAAAAAAAABAAAAAQACAAABAAABAAABAAABAAABAAAAAAAAAwEEAAAAfHuSkpGoDwwAhoRu0MiWIxsAAAAEAgEKAAABAAABAAABAQEDAgEAAAAAAAAABAEAtrb21tb/PTMSr6WE//+mX0oAAAAAAAAEAAABAAABAgAAAAAAAAAAAgIGL0qUV3G8aYzsc5b1WnCgU2mZRkpYNThGiIqeLS9DAgAAAgAAAAASRE1hiKDRnbXmdpbhnr3/1uT/3+3/3vX/xNv/gp3nZH7J7un/jYmzBAEAAAAAIDdwnrXuyef/pML30dvvy9bpmJaZiYaKnpudzMrL6/r/1uX/gYy6CxdEAgABAAAAJT99pL78nrnvqMP60tHQQ0JBr6hdtK1hQz04Pzo1X2Bot7nB1+X/IjBQAwAAAQAAAAgzi57JiaLnyeL/koxsaGJC7uiof3o6WlhnOjdGRUEsV1I9q7PCjZalAAAAAQAAAAADEBQeVGia1un/wsC2Pz00pZ91UUshZl1SUkk+rKRN+vKbf4F8tbayAAABAAABAgEAAAAAAAAQp6y8+v//kZirMSojPzgx07Jumng1ycN/nphUNzhDqKm0AAABAAABAgAABAEAAAAaN0Fbvcnn7/v/xsTFZWJkcWBSSTcpT0UtTEMqSVFusLnWAAADAAADAwAAAAAABRtLT2WUYYXbkLT/2ub/7vr/zdLiq6/Al5iiqau1prnmT2GPAAABAAABAAABAAABBgMAAwAACBY1OEZmSWWoZ4PGlKDBpbDRoqm8aXCDCwsNAAAAAAABAAABAAABAAABAQACAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgADAAABAAABAAABAAABAAABAAABAQACAgADAQACAQACAgADAgADAwEEAwEEAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
15
|
+
"AQACAAABAAABAAABAAABAAABAQACAAAAAAAAAAAAAQAAIBwZAAABAAABAQAEAAADAAABAAABAAABAAABAAAAAwMDAAAAYF1lvL3YICE8dGwp///Dd3BPAAAAAQAJAAAIAAABAAABAAAEAgIGBAMAAAAAAAAAioWW5ej/SUyLcFsL79qKjIRlCwMAAgMNAAAEAAABAAACAAAAAAAAAAAAGR4sQWC3YH/Xd5rubZDkX3quSWSYGB5GkpjB0M3eFhMjAQAAAAAAAwsYUlpndI/Kd5LNiaXow9//4ez/7ff/4PX/y+D/lKz2i6LtubXYHBk8AAAEAAADWXSywNr/xOH/q8jv4OXun6Stg4GCd3V2mJaXxsTF5vX/0eD/SFNxAAATAAAJBgsZcJDUr8//lKnT1On/dG9KeHRP0suLfHU1NC81QTxCT09dtrbEytj3BRMzAAACAAAAQV6dqMX/n7Dhzd//QToA5d6elpJxaGVEPjtFPzxGhX1NiH9QsrrJcXqJAwAAAAAAAAQgRVJukKLQ5vn/Z2JJa2ZMiH1dVEophnpRcGQ79O6evbdnaGx61NjmAAABAAACAAAAAAAALjdL4On94OX1WV5uNCMQbV1J1rZxfl8axMKSQj8QQkVbxMjeAAABAAABAQAAAgEAAAATYGiB3eX97fX/s6+ucG1sZFlWPDEuQzw0UElBoLDforPhAAADAAADAwAAAAAADyleUGufbZHhm8D/3un/6fX/2eDzydDixsrezNDkdYGhHilKAAABAAABAAABAAAABAMCCAYFEB5COEZrRmKmZH/DgpTCiZvJbnWIExotAAAAAAAAAAABAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwEEAgADAAABAAABAAABAAABAAABAAABAAACAQEDAQACAQACAgADAgADAgADAgADAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
16
|
+
"AAABAAABAAABAQACAAAAAAAAAAAAAgAAfnpXZF88AAAAAAAAAAAAAQAEAAABAAABAAABAAABAgABAAAALiw8qae3PDw+QUFD//+R/+l4IyMjFBQUIyMoAAAAAQAAAAAAAAABAAABAwIBAAAAWFJ4+fP/dnyiERc9dnFYXlk/GBk7sLLT7+n/HxkzAAAAAQEBAAABAgIEBAEAAAAAFyprd4vLfqDrja/6hqjvepviYXjEhJvniourAwQjAAAAAQAABAAAAAAAAAATNkNnkKzvyeX/6/D65+325+zz7/T75fv/yuD/c4a2BBhIAAAABQIAAAACPUJJgZnKmLDh2N3pl5uobGZhUkxIVFJYYF5kb290qamu3/P/e4/BAAAKAAAAJTtrwNb/wNf/wNf/cGxJhH9dxMCdWlYzPzw7Ozc2hn5Ropptd3+LzdbhoK7NN0VlSWKisMr/orXforXfUEofq6V6a2dLdnNXQj4tXlpJrad9lI9lV1hRmJmSxuH/oLv2DSVdmrLpnrPsxdr/WlY8aGRKjH5McGIw5cWElnY1jYVTg3tJPzw2npqUtc//kavtAAACIicwSFqFqLvm6+77a257QTg1OzMvinFWWkImOjQxT0lGf4aZ2eDzn73/VnS8AwAAAAAAAAAADwwQpq678/v/2t/mt7zDm6Cpn6StsrO719jg5u//W2V/KS4+AAISAAABAgADAgIEAAAAKESFhKDhsMr/xN7/1+f/2Oj/y+H/ornsLC0zAAAAAAAAAQAAAAABAAABAAABAAACGSM4FyE2JjxuNkx+OFKNPFaRKT5rITZiAwQKAAAFAwECAQAAAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAQEDAQEDAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
17
|
+
"AAABAAABAQACAAAAAAAAAAAAAAAAbWhTioJlCgIAAAAAAAAAAAAAAQACAAABAAABAAABAQACAAAACAUGb3GMP0JdNSgA8OSo//utW0kAICI9bW+KFxQWAAAAAQAAAAAAAAAAAgABAAAAMC01+fb/qafZFhccVFZbZ2dnFBQUfn2y////XVhoAAAAAwIBAAAAAAAAAwECAAABAgIGWXC7f5fhfZ/mjK71i6vzgaDoe5LddIvWEhMbAAAAAwIBAAAABAEAAAAAAAAqVmaUrMT22fL/7fL54ufu5enw7PD32/P/t8/7YnWiAAg1AAAABQIAAAAAHyElUWeavNL/zMzRenp+VlFSUUxNUE1PVFJTdXV3w8PFyd//WW+iHB4jAAAAN01zxt3/zd33oK/KX1YsxLuRe3dmPjspPzsvZ2JXycCWcGc9k6C3ydbtw9j/RluFhaXprMv/wcvZWGJwhX9UfHZLkYx1V1I7TEY1jIZ1d3FGlI9kTVNawMXMqcn/iqn1X33Gqcj/wsvohI2rS0Ubl5FnemEnuaBm0LJ1fF4hlpBlWVMoYWh7ydDipsb/b5DeARI6aHqhip3K4fT/kZCXRkVMOC8oTEM8WU1EPDAnQz9Id3R83u7/nq7Zd4u9EydZAAAAAAAAAQQRZml27PD95Oj1yMzTsLW8rrO8wcXP29/t8vX/iY2eCg8fAQAAAAAAAQACAQACAAAAAAAAS2akor37wdf/y+H/zeH/xdn/qcT/YHu5AAAAAAAAAQAAAgABAAAAAAAAAAAEBAQJGylJGylJLUR6NEuBNEuDLUR8GShKGilLBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
18
|
+
"AAABAAABAQACAAAAAAAAAAAAAAAAd3Nbf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKb3GMNDZRPzEA/e+u//CoTTwALC5Jc3WQExESAAAAAQAAAAAAAAAAAwECAAAAQT1F//3/m5nLExYZWl1gX2FiEhQWi4q+/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYXjDfZTffp7mja31jKz0gaDofZbdbYXMCwwSAAAAAwECAAAABQIAAAAAAAAtWGiXrsT23fP/7fL54ufu5enw6+/22vL/ts36YnWiAAY0AAAABAEAAAAAGxsgUGaZwdf/ysrMd3d6VlFSUUxNUE1PVFJTdnZ4xMTGxt3/WW+iIiMpAAAAOExwxdn9zd31m6vDYlkvxLuRc29gPjssPjoubWhdycCWbmU7l6S7ydbtw9r/RV2Giqnurcz/wsrXVF1pi4Vae3VKkYx1Uk02TUk0jYl0d3FGk41iTVdfvsjQqMj/iaj0ZoXRp8b/xM3me4SdUEofmZNoeF8iwqhsy61weFoel5FnU00ja3SIyNHlpsb/aYrYBRZCb3+skaTP4fT/iIaNRURLOiweZFZIbl1PPy4gQ0JLf36I4PD/lqbRcYSyDSBNAAAAAAAAAwgWb3SC7vP/4ebzxcrRrrO6rbK7w8jR3uLv7/T/foORBQoYAAAAAAAAAQACAQACAAAAAAAAUGuopcD9wtj/y+H/zeH/w9f/psH/WnW1AAAAAAAAAQACAQACAAAAAQAAAAAEBAQJGylJHCpKLUR6NUyCNEuBLEN4FyZFGShIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
19
|
+
"AAABAAABAQACAAAAAAAAAAAAAAAAdnFafnhdAwAAAAAAAAAAAAAAAwACAAABAAABAAAAAQAAAAAACwkKbnCLMzVQQTMA/e+u//KpTz0AKi1Ic3WQExESAAAAAQAAAAAAAAAAAwECAAAAPzxE/vz/m5nLExYZWl1gYGJhExYUjIu9/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBgUMYXjDfZTffZ3mjKz1iqnyf5/nfZbdbYXMDQwTAAAAAwECAAAABQIAAAAAAAAtV2eWrMT12vP/7vP65enw5uvy7PD32vD/tcv7YXOkAAQ1AAAABAEAAAAAGxsgUGaYwNb/zMzReHh9VlNUUlBRUlBRWFZXd3d6xcXIxdv/WG6hIiMpAAAAOExwxdn9zNv0nazEZFoww7qQcW5fPjssPjoubWhdyL6UbWQ6maa9ydbtw9r/RV2Giqnsrcz/wsrWVl5pi4VafHZLkYx1Uk02TUg2jYh2eHNIk41iT1hgvsjQp8b/iKfzZoXRp8b/xM3meIKaUUsgmZNodV8lt6FnvqRtdVojmJJnVE8jaHGFxtDkpsb/a4vZBBdCboGskqXQ4PP/hIOKRENKOCkaaVpLdWFPQS0aQkNJe3yC3+//mKjTc4WzDyFPAAAAAAAAAwgWcXaE7/X+3+XuwsbNq6+2q6+5wcXP2+Dt7/T/goaUBgsZAQAAAAAAAQACAQACAAAAAAAAUWynp8L9w9n/zOL/z+L/xdn/qMP/XXe1AAAAAAAAAQACAQACAAAAAQAAAAAEBAQJGylJHCpKLkV7Nk2DNk2DLUR6GCdGGShIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
20
|
+
"AAABAAABAQACAAAAAAAAAAAABgIAc25ZRkEwAAAAAAAAAAAAAgABAwACAAABAAABAAABAQACAAAADwwNRkpgBgoga1oJ///A3c+YKBoANztRWV1zAgABAAAAAAAAAAAAAAAAAgICAAAAd3R+////b26fJiEMiYRvXl1bISAfubbn7+3/JyMsAAAAAgABAAAAAAAAAwECAAAAJyYviJvocITRX4bZaJDibpLmZYndgpffbYLKBAQGAAAAAwECAAAABQIAAAAAAAIwS1uKk67szOf/7fb/7fb/7Pb/5/L/vd3/i6vtVGeUAAAtAAAABAEAAAAABAQJQ1iRvtP/5uv0qK22iIiKe3t9fnx9j4yNtLnG6O37nrf1U22rLCwwAAAAJTRTtMPiytfur7zTTUhFNzEvOjQxPzo3PDg3Ozc2OjQxaWRhxdr3t8zpyuD/RVuLhKHgu9j/xcrXS1BdMy0oRkE8S0ZIRUFCQ0FCQ0FCRkE8LykleIaaucbapsT/g6HwbozWrcv/xsvURktUNzQzRkNCXUstnYtteGxXSTwnSkRCJyEffYqkusbgp8j/XX3NECNTfZHBxNDusLzaPjw9NTM0Ri4Gp49nd2FGPigNODg7b29x2Ov/iZvGaXegBBI7AAAAAwECHyc2vsbW4ebtoaathYKBhoOCiYaKmJaZwMTR7/T/gYSRAAMQAAAAAAAAAQAAAAAAAAAACQkNiqDSzOL/4O7/3+3/3e7/2On/udP/aYS8AAABAAAAAgADAQACAAAAAQAAAQEFAQEFGi5UJzthQViQRl6WQ1mMNUt+GihIFyVEBAQJAAADAAAAAAAAAAABAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAAACAQEDAgADAgADAgADAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
21
|
+
"AQACAAABAgAFAwEGAAAABgQFVlA+GxYEAAAAAAAAAAAAAQACAAABAAABAAABAAABAAAAAQAAAAAAAAAAAAAAfXdZ//+6sp9BBgoYYmZ0RkRKAAAAAQAAAAAAAAABAAABAgEAAAAALSxBtLPIUE9NSklIxLV1cWIiQkR17vD/tq/DAAAGAQIAAAEAAAABAAABAwIBAAAAW1Z67ef/dovTPFGZWILWW4XZaYjkfp35e4a0ERxKAAAABQIAAQEFAAADAwAAAAAAAAxBbX6zosD+yeb/2ej/2ej/xOD/nrrpcZv+aJL1LTVQAAAJAAAAAwAAAAAKAAAAZ3ur3fD/5ebsw8TKsrO5rK2zrq+32tvk0uj/iZ/PmLPnhaDUMTY/AAAEAAAITVZl1t7tiZGgSUVEPDg3PTgvQTwzkoxhfHZLlp6r5Oz5kLLnxOb/ma7nEidgSFl+3e7/gn9dqKaDw7yIVE0ZNDFBb218yb5l9OmQRUM4xcO5oL36n7z5mrP1Izx+e5DJssb/SkgvdXNa9OubgXcob2ZeTEM7dW0p3dSRNjEowbyzn77/mbn/d4y5AAs3NEuEtMv/ZmRZNzUqnY9dZVclxad4bU8gTEg+PjowdnqK+fz/c4a5O0+BDQ0QAAAAAAAQMzpMub3LhYqYRj47RDw4gm5bbVlGaGlxs7S89Pn/i5CgAAAAAAAAAAAAAQAABAEAAAAAGR4qsLXC2+X/0933z9jt3+j94vP/0eH/i6foLUmKAAADAQEFAQACAAABAAAABAIDAAAAAAAESl6NZHendIvBZ360UmmhMUmBGCU+GCU+AQEDAAABAAABAAABAAABAAABAgADAQACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAgADAgADAQACAgADAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
22
|
+
"AgABAgABAgAGAwAIAQAANzUsFBELAAAAAAAAAAAAAQACAAABAAABAAABAAABAAABAAAAAQAAAAAAAAAAXUwA//++pJl4IhgAcXCFISA1AAAAAgABAAABAAABAAABAAABAQAAAAAAJSg+Oj1TYU8P1sODdm1ic2lf////dG6gAAAAAgEAAQEDAAABAAABAAABAQAAAAAAxcHr3dj/LUOETGKkWHzMZIjYgprycIngPEVeAAIaAAAABAEAAAACAAABBAEAAAAARVOQlqTgrsn/xN//zeL/wdb+kbP9fqDrdpn2UnXSFxYcAAAAAAAAAQAAAAAKAAAGhJO16Pf/3uDkyMrNubrEurvF3+b55ez+j6vfpsL2wdn/YXqrCxAZAAAFAAAAVlpk1NjoYWV1TEhLPTg8U08pmZRvZ2VatrSp3fP/mK7Rs9L/m7v/SFR7AAAcCAwWlpqkkYtgiYNYQT5EODY8jYpQ2NSaz8iKRT4B0d//rbvkk7X/oML/PktvAAAcTFh4c36fkIpF8uynem5SZFg8RD8cj4pnxr6IPTUAzN3/qbrmg6LucJDbCAkPAAACOkRpdoGmKiYAtK+IjHM1vKJlZFQ8XU01NjQ1kpCR7/3/mafQLDVQAAYhAAAAAwAAAAAMbHCBfn6DPT1CPSgQgm1UYFFLYlNNrrbF8Pn/vsXbJy5EAAAAAAAAAQACAAABAAAAAAAAgYWWwsbXt7vPubzQ09rt7vX/0+v/oLfwZYTNJUSNAwECAQAAAAABAAABAQAAAAAAAAAAOD1Gf5C7i5vGf5TLaH20S2WgLUaCGiM3ExwwAAAAAQACAAABAAABAAABAQACAgADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAgADAgADAgADAgADAQACAQACAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
23
|
+
"AgABAgABAgAFAQAEBQMANTMpCQUCAAAAAAAAAAABAQACAAABAAABAAABAAABAAABAAAAAQAAAAAAAAAAfW0e//+8enNbLSYPcXGDDAweAAAAAQAAAAABAAABAAABAAABAQAAAAAAHCA0KCw/fGYe4MqCYFhZmZGS//z/QzxpAAAAAgEAAQEDAAABAAABAAABAAAAAAAAy8rzzcz1Jjx4UmilWHjKbIzeg53waILWOEJXAAAUAAAAAwAAAAACAAABAwAAAAAAV2Whmafkrsn/wt3/y+D/us/5iq79fKDvd5ryUXTLFhYaAAAAAAAAAQAAAAAKAAAFhZS26Pf/3d/iycvPurvFwcLM5u//2+X5i6jZtNH/vNP/V26kBggSAAAGAAAAXWFr0tbkYmZ0TUlKOzY3ZF02mZJsZGJfzMvI0ej/k6vTttb/kK/7O0NlAAAWBAULk5SakIlidW5IPDhBPzxEop1Z6eSgp59tV08c3/L/m67Zn8H/lLb/KjdTAAAUPkZfcXqSqKRZ7OedZVtIZFpGPzoQsqyCoZlsUEga3fD/lqndjKjuWXW7AAABAAABNj5gbHSWNjMGvLmMj3U3uqBiWUs2XU86NjY4r6+y6/v/hpfCISg9AAATAAAAAwAAAAALbHB+dHFzPDo7Qy0SiHFXVEtEZ15XvsXY7/b/sLnRGSE6AAAAAQAAAQACAAABAAAAAwMFjJGfu8DNr7TEtrvL197w8Pf/y+X/mLLtYoLLJkWPAgABAAAAAAABAAABAgABAAAAAAAARktUipjBkqDJf5TLZnuySWSfKkWBGiM1ExwuAAAAAQACAAABAAABAAABAQACAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAgADAgADAgADAgADAQACAQACAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
24
|
+
"AgABAgABAgAFAQAEBgQAMzAnBgMAAAAAAAAAAAAAAQACAAABAAABAAABAAABAAABAAAAAQAAAAAAAAAAhnYl//+5b2hTMSoWb29+CAgXAAAAAQAAAAABAAABAAABAAABAQAAAAAAGyAwIyg4hG8j4cyBW1RXopue//n/ODFdAAAAAgEAAQEDAAABAAABAAABAAAAAQAAz832yMbvIzt0U2ukV3fJbY3fg53wZn/TNj9UAAATAAAAAwAAAAACAAABAQAAAAAAXWunmqjlrsn/wt3/y+D/uc36iq79fKDvepnyUnHKFhYaAAAAAAAAAQAAAAAKAAAGhpa36fn/3uDkyszQubrEwsPN5/D/2eL2i6jZt9T/u9L/VGyhBQYPAAAFAAAAYGVu0tbkZGd1TUpJOzc2Z2A6mJFrZWRi0dDPz+b/k6vTttb/jKz3Nj5eAAATBAYKk5aZjYZib2hEPDhDQT1IpqBb7Oahn5dpXVQn4PP/mazXocP/kbP9JzBLAAASPkVbc3qQrqlf6eWaYFhGYlpJPjgNt7KGmZBmVkwi3/P/k6fajKjsUm6yAAAAAAAANz9faXGROjcIvryMkHY4uqBiWEo1XU86Nzc6tra56fr/g5O+HiU3AAAPAAAAAwAAAAAMbXF/b25tOzo4RC8SiXRXUklCaWBZw8rd7/b/rbXNFx83AAAAAgEAAQACAAABAAAABgQIj5Ohur7MrrPBtrvJ2ODv8vr/yuT/l7DuYILKJUaPAgABAAAAAAABAAABAgABAAAAAAABSk9YjZvCk6HIf5TLZnuySGKeKkWBGiM1ExwuAAAAAAACAAABAAABAAABAQACAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAgADAgADAgADAgADAQACAQACAgADAQACAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
25
|
+
"AgABAgABAgAGAgAGAwAAMy8pCwgEAAAAAAAAAAAAAQACAAABAAABAAABAAABAAABAAAAAQAAAAAAAAAAcWUX//++hn5kIRkAb2+BFhYnAAAAAQAAAAABAAABAAABAAABAQAAAAAAJik9LC9Dd2Ea4syFaWFehX16////XViEAAAAAgEAAQEDAAABAAABAAABAAAABQME2Nf/ysnyJTt3U2mmWHjKaYrbhp7zbYTZN0FWAAEWAAAABAEAAAACAAABAwAAAAAAV2allKTiq8j/vtv/yd7/us/7iq79e5/udZj1TXDNExMYAAAAAAAAAQAAAAEKAAAJhpS56ff/4eTnzdDTvsDKwcLM5ez+4ej7i6fbrMj8vtT/XXOmCgsWAAAGAAAAXmFu2NvpZ2t4UEtMPTg6WFEvl5BuaGVfw8C62vD/l63StdT/l7b/Q09xAAAYCxAZm6Cpj4hhenNMPTpCOjY+mZNW4NqdvrZ9S0MK2OX/qLXhmLr/nb7/N0VnAAAaV19+dX2doJhS8umkbWRLYFc+RD0ZmpNvwbmEQTgE0uL/pbXkhqXua4nSBAYKAAACRVB3b3qhLywBvLmNjXQ2vKJlYFE4YVI6NjQ1l5SW8P7/lKLLKjRJAAUaAAAAAwAAAAETcXiLdnR1PTs8PikPhnFXWU1GXVFKrrXI8vn/vMPZISg+AAAAAAAAAQACAAABAAAAAwECi5CevcLQsLXDtbrI1Nnp8PX/0+v/oLfvYoTMJUaPAwECAQAAAAABAAABAgABAAAAAAAARElSipi+k6HIgpjLaX+zTGahLUaCGyU4FB4xAAAAAAACAAABAAABAAABAQACAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAgADAgADAgADAgADAQACAQACAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
26
|
+
"AAABAAABAwAEBgIIAwEGAAAAGBQTCAQDAAAAAAAAAAAAAQACAAABAAABAAABAAABAAAAAQAAAAAAAAAAAAAAPToo//ais6RQAAAADQsMEhIXAAAAAQAAAAAAAAABAAABAQAAAAAAFxcmi4uaPj06UE9L9t2BvKJGKSpMvL3f1M3fDwgZAAAAAAAAAAABAAABAgICAAAAV1Bu//3/jJrXOEaDaYzQYYTIYXfLnbP/s7PbGBhBAAAABAMAAAACAAABBAEAAAAABBBFcX2ze5/1mr7/mrv9mrv9j7P/dJjlZ4/wYYnrHyY7AAAAAAAAAwAAAAAEAAAAPVeXrsj/6fP/6PL/6e7z6e7z6+/28PX8u9T/eJLUdI/GPFePEQ0KAAAAAAETVFtuw9Dp0N32i4mKZmRlWFZZVlNXXltffnx/ytLh3ub1lrTnz+3/lKXRDBxJZ3uf5fn/tbW3Ozs9OjQxQTs4Qj49Qj49RD48OzUzODg9w8PIp8P0ob3up8H/OlOYkKXextv/WVNPNjAsSUNDSkRET0lES0VBQz8+SUVELCcoioWGttD/mbPunrn5Ei1tQViQwtn/cG5xKScqSkI0VEw+tp1shWw7QTs4Pzo3LS0xycnNobz8Z4LCPUZaAAAPAAAQOkFTu77LcHSBPzgwNC0llnhSdlkzOjU2ZF9gwMPQ29/sKC02AAAIAAAAAgAAAQAAAAAAIic1wMTS0dbmub3NtLnCwsbQ0dny6fL/yd//V22gAAAAAAADAgADAAABAAAAAwMDAAAABQYPZHahiJrFkqTLi53EfJDDWW2gJjRYFiNIAgIEAAABAAABAAABAAABAAABAgADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAAAAAAAAAABAQACAgADAgADAgABAgABAgADAgADAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
27
|
+
"AAABAQACAgADBAAFAwEEAwEEAAAAAAAAAAAAAgAGBQEEBQEEAQAEAAADAAABAAABAAABAAABAQAAAAAAAAAAAAAAIRwApJ96Uk02AAAAAAAAAAAAAAAAAAAAAAABAAABAAAAAQAAAAAAKCYpW192BAgfinQU//+uw7eGIhcAYWJ/fn+dBgQFAAAAAQAAAAAAAAAAAgICAAAAn5mh+fz/en26aHeNobDGfpPAX3Sgzcv/4uD/GhgeAAAAAgABAAAAAQAABAMAAAAUYGaEgqD/dZP0bZ3/YZH1bJj7b5v+b43sdZPyNj1TAAAMAgEAAgEAAAAAAAAAGDaIZoTWhaDetdD/2+Tz5e385e7/2OHzrsr/hKDkaYjTEjB8AAAAAAAADBYpfIWZhqHWqMP39fr/1Nngu7zEra62rq+3vL3F29/s8PT/lLDnmbXsl6S9GCU+gp7Wyub/wdDmydjuaWVmQj0+Ozc0Pjs3PDg3PTo4RkJDhH+B0+b/pbfWwd//dZPbi6nypcP/xMnWUldkMSkmSkI+SkVJQj1BQj9DRkRISEI9LigjfoigucLapcX/bo/gOlOWpL3/ytHkQklbNzEvRkE+Ukk1fHNfZ19PRj4uSEI9JiAbfouvvcrui6LaGTBoAAAIHCEuhYyfw8rdSkhLNTM2SzEDtZttgmlJPycGPTxDg4KJ2uH2QUhdCgoPAAAAAQAAAAAAAAAAj5Od5u3/t77TpJ6et7Kysq2wtbC00Nv64Oz/TU9XAAAAAAAAAgABAAAAAgICAAADAAAATV+GjJ7FpbLNoa7Koa3LmqbEZnGQIy9NAAAAAgIGAQAAAAAAAAABAAABAQACAQACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgADAAABAAABAAABAAAAAAAAAAABAAABAgADAgADAgADAgADAgADAgADAwEEAgADAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
28
|
+
"AAABAAABAAADAQAEAwEEAgADAAADAAAAAAAAAgAGAgADBAIFAQAEAAADAAABAAABAAABAAABAQAAAAAAAAAAAAAACAUAdnRbS0g1AAAAAAAAAAAAAAAAAQAAAAABAAABAAAAAQAAAAAAEhARREZdAAIYaFQA//+z28ySJxgAODtWbW+KCwkKAAAAAQAAAAAAAAAAAgICAAAAhH+F+///gYW+bXeCq7XAkqG8YnGMvbvz9fP/NTA2AAAAAgICAAAAAQAABAMAAAAMYGF8kaj/e5LwaJn/W4z3ZJH3bZr/dJDvf5v7P0NaAAAMAwIBAgEAAQAAAAAAEi+BZYLTdJLbn73/zNfs1+H22OH6y9TtosH/fJria4nUEjB8AAAAAAAACRIjc3yNg57Wl7Lp8/f/6O3609jhxsvUyMzT0dbd5+z58PX/ka3lka3lj5uzFiI6fprSzen/sMHb1+f/jIqNUE1RQz8+Qz8+QT08REE/UE1RjIqN1+n/qLvXwuD/epjgjKvzosH/wcrbbneJLSUhRDw4SENEQj0+Pz1BRUNGRkE8MSwndX6SvMXZpML/d5bnOFSYoLz/y9TpTFZrMy0oSEI9TUQ6aWBWYVpQRj81SkQ/KiUgYWmJxs/uj6nhIDtzAAAIHiIvfoWaxs3iSEVLNTM4QyoBqZFnmn9YQygBNzc8ZGRo2N/yW2J1DAwRAAAAAQAAAAAAAAAAj5CY5u3/sLfMmJCMsKilsqmopp6dw8zp5u//bW52AAAAAAAAAgEAAAAAAgICAAAEAAAAUmCEk6HFq7TPrbbRq7TPpK3IdH2aLjdUAAAAAQEFAgABAAAAAAABAAABAQACAQACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgADAAABAAABAAABAAAAAAAAAAABAAABAgADAgADAgADAQACAQACAgADAwEEAgADAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAQACAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
29
|
+
"AAABAAABAwACAwACAAAAAAADAAAAMy8pKigfAAAAAAABAAAAAAABAQACAAABAAABAAABAAABAAAAAAAAGRwqAAAMPDAA//ez692dLR8ABgoaJyo7AAAAAAAAAAABAAABAAAAAwECAAAAUU9U9vb/dnakOC0ItKiDoZl6LycIkZG+9/f/PzxEAAAAAgABAAAAAAAAAwECAAAAMy83rLf/gYzWSnPLXobfXoXbT3bMiZTen6v0HhoiAAAAAwECAAAABQIAAAAAAAApQU16a4/lncH/zOD/z+L/z+H/yNr/l7v/dJjsTVqGAAAqAAAABQIAAAAAAgABM0yRkavv5e7/2+X6xsbLubm9urq+xcXK1+H25O7/kKzyOlabDAoLAAAAKDdXtcTktcjoy97+iIWGRkRFOzc2Pjs6PTo4Pjs6SkZFjImIz+H/ssTlu8/1NEhuiqnptdT/xM/idoGUKSEeRT06RkRIPz1BPz0+RUNESUE9MSkmeIOXvsndrs3/haXudZTgp8b/w8vaUVloNjAwSEJCTEQ0bmZWa2JSSD8vSUNBMy0qU1ptwcjap8j/bo/fFCpegpjLu8nrrbvdNjQ1NjQ1Si8Du6B0sJFnSywCNzU2PDo7tcLms8DkfpLEFilbAAAABQUFIik8yM/h2driiouTa2JfdW1pe3Btc2hlkZaf3+TtwcjaGiE0AwECAAAAAgEAAAAAAAAAEhMZpbfh3vD/5/T/4O3/4Oz/5/P/2Oz/oLTkEhMZAAAAAQAAAgABAAAAAAAAAQEFAAADGi9ZNUp0VG+mYn20YXerT2WYLDpeFyVJAQEFAQEFAQAAAAAAAAAAAAAAAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAAAAAAAAAABAAABAQEDAQEDAgADAQACAgADAgADAQEDAAACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
30
|
+
"AAABAAABAwACAAAAAAAAAAAAAAAAdG9YfXdbBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbW+KMzVQQTMA/e+u//KoUD4AKi1IcHONExESAAAAAQAAAAAAAAAAAwECAAAAPzxE/fz/mpnLFBcaWl1gYGJkExYXjYvA//3/TUpXAAAAAgICAAAAAAAAAwECAAABBgUMYnrEfpbgf5/nja31jKz0gaDofpfebYXMDQwWAAACAwECAAAABQIAAAAAAAAtV2eWr8X32vD/6+/24OXs4ufu6O302+//us39ZHakAAY0AAAABAEAAAAAGxsgUmaZw9f/xsbJdnZ4U09QT0pLUEtMVFBRdHR2wcHDxt3/WnCkIiMpAAAAN0tvxdn9zNv0mqnCX1kuw72Sd3RlPToqPjoubmlexsGWbGY7lKG5ytfuxtv/SF2Jiqnurcz/w8vYVF1pi4ZYfHdJkItxVFA2T0o1jYl0dnFDko1fT1NgwMTRqMj/iaj0ZoXRqMj/xMzlfoafTEkel5NoeF8iwadrza1xfFsglpBmUkwibXSKyM/lp8X/bIrbBhdDcIGtkKLN4vX/j42URkVMOjApUEY/WU1EPTEoRENKg4KJ4fL/lqbRdIa0DyFPAAAAAAAABAkXbXF/7fL+5Oj1y8zUtrfAs7fBxcrT4OXz7vP/fYKQBgsZAAAAAAAAAAAAAAAAAAAAAAAATWimpL78wNT/yd7/yd//wNb/pcD/WXS0AAAAAAAAAQAAAQAAAAAAAAAAAAAEBAQJGypMGypMLUR6NEuBNEuBLUR6GihIGylJBgYLAAADAQAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAAAAAAAAAABAAABAAACAQEDAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
31
|
+
"AAABAAABAQACAAAAAAAAAAAAAAAAdnFaf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbnCLNDZRPzEA/O6u//KpTz0AKi1Ic3WQExESAAAAAQAAAAAAAAAAAgABAAAAPjtD/Pv/mpnLExYZWVtfX2FgExYUi4q8/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYXjDfZTffp7mja31jK3ygaHmfZbdbYXMDQwTAAAAAwECAAAABQIAAAAAAAAtV2eWr8X32/L/7PD34ufu5Ojv6+/22vL/ts36ZHakAAY0AAAABAEAAAAAGxsgUmaYwtb/ycnLd3d6U1FSUE1PUE1PVlNUdnZ4w8PFxt3/WW+iISIoAAAAOExwxdn9zNv0m6vDYlkvxLuRdXFiPToqPjoubWhdyL6UbWQ6l6S7ydbtxdr/SF2Giqnsrcz/wcvXVF9ri4hbe3dLkYx1U083TUk0j4p1d3FGkoxhTVdfvsjQqMj/iajyZYTQqMj/w8zlfIWeUEofmpRpe2Elwadrza1xfV0hmJJnU00ibHOIyM/kpsb/bIzaBRZCb3+skaTP4fT/ioqPRUVKNy4mUklBXk9GPi8nRENKgX+G4fL/lqbRc4WzDyFPAAAAAAAAAwgWb3SC7vP/4uf0xsvSsra9sLW+w8jR3uLv7/T/f4SSBQoYAAAAAAAAAQAAAQAAAAAAAAAAUWumpsD7wtj/yuD/yuD/wtj/psH/W3a2AAAAAAAAAQACAQACAAAAAQAAAAAEBAQJGilJGypKLUR6NUyCNEuBLUR6GCdGGShIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAAAAAAAAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
32
|
+
"AAABAAABAQACAAAAAAAAAAAAAAAAdnFaf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbnCLNDZRPzEA/O6u//KpTz0AKi1Ic3WQExESAAAAAQAAAAAAAAAAAgABAAAAPjtD/Pv/mpnLExYZWVtfX2FgExYUi4q8/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYXjDfZTffp7mja31jKz0gaDofZbdbYXMDQwWAAACAwECAAAABQIAAAAAAAAtWGiXrsT22/L/7PD14ufs5Ojv6+/22vL/ts36YnWiAAY0AAAABAEAAAAAGxsgUGaYwNb/ycnLd3d6VlFSUUxNUE1PVlNUdnZ4w8PFxt3/WW+iISIoAAAAOExwxdn9zNv0m6vDYFovwryRdXFiPjssPjoubWhdxcCUa2U6l6S7ytfuxdr/SF2Giqnsrcz/wcvXU15pioZaenZKkYx1U083TUk0j4p1dnBFkoxhTVdfvsjQqMj/iajyZYTQqMj/w8zlfIWeUEofmZNoemIjwalrza1xfV0hmJJnU00iaXOGxtDkpsb/bIzaBRZCb3+skKLN4fT/ioqPRUVKNy4mUklBXk9GPi8nQ0JJf36F4PD/lqbRc4WzDyFPAAAAAAAAAwgWb3SC7vP/4uf0xsvSsLW8r7S9w8jR3uLv7/T/f4SSBQoYAAAAAAAAAQAAAQAAAAAAAAAAT2mlpcD7wtj/yuD/yuD/wdf/psH+W3a0AAAAAAAAAQACAQACAAAAAAAAAAAEBAQJGShIGypKLUR6NUyCNEuBLUR6GCdGGShIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAAAAAAAAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
33
|
+
"AAABAAABAQACAAAAAAAAAAAAAAAAdnFaf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbnCLNDZRPjAA/O6t//KpTz0AKC5IcHaQExESAAAAAQAAAAAAAAAAAgABAAAAPjtD/Pv/mpnLExYZWVtfX2FiExYXi4q8/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYXjDfZTffZ/mi630jK3ygaHmfZbdbYXMDQwTAAAAAwECAAAABQIAAAAAAAAtWGiXrcP12/L/7PD14ufs5enw6e712vL/ts36YnWiAAY0AAAABAEAAAAAGxsgUGaYwNb/y83Rdnh8V1RWT0xNUk1PW1dYdnZ7xsbLxt3/WW+hISIoAAAAN0tvxdn9y9rwoK/FNjAsOzUwQT08Qj49Qj49Qj49PzgxOjMsm6m9ydfrxdr/SF2Giqnsrcz/wMnaWWJ0NS8qRT87SEI9UUtGUUtGRkE8SkQ/PTczTVZlvsbWqMj/iajyZYTQqMj/w8zggoufKSUmQz4/VD4Rwat90K9+Xz4NQj9DJiMnb3aJxs3gpsb/a4vZBRZBbn6pkKLN4fT/kI2PS0lKPDMoUUg9XU9DQzUpSEhKhYWI4PD/lqbRc4WzDyFPAAAAAAAAAwgWbnOB7fL+4ebzxcrRsLW8r7S9wsbQ3eHv7vP/f4SSBQoYAQAAAAAAAQACAQACAAAAAAAAT2mlpcD7wtj/y+H/yuD/wdf/psH/WnW1AAAAAAAAAQACAQACAAAAAAAAAAAEBAQJGShIGypKLUR6NUyCNEuBLEN4GCZFGihIBgYLAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
34
|
+
"AAABAAABAQACAAAAAAAAAAAAAAAAdnFaf3peBAAAAAAAAAAAAAAAAQACAAABAAABAAAAAQAAAAAACwkKbnCLNDZRPjAA/O6t//KpTz0AKi1Ic3WQExESAAAAAQAAAAAAAAAAAgABAAAAPjtD/Pv/mZjKExYZWVtfX2FiExYXi4q8/fz/TUpXAAAAAwECAAAAAAAAAwECAAABBQQLYHfCfZTff5/njKz0jKz0gaDofZbdbYXMDQwTAAAAAwECAAAABQIAAAAAAAAtV2eWrsT22/L/7PD14ufs5Ojv6+/22vL/ts36YnWiAAUzAAAABAEAAAAAGhofT2WXwNb/ycnLd3d6VlFSUUxNUk1PWFNUdnZ4w8PFxt3/WW+iISIoAAAAN0tvxNj8zNvym6vBYVgvwrmQdHBhPjssPjoubWhdxL6UaWQ6l6S7ytfuxdr/SF2Giqnsrcz/wcvXU15pi4lZendIkIt0U083TUk0jYl0dnBFkYtgTVdfvsjQqMj/iajyZYTQp8b/w8zlfIWeUEofmZNoeGEjwalsza1xfFsgl5FmU00iaXOGxtDkpsb/a4vZBRZBbn6pkKLN4fT/ioqPRERJNy4mUklBXk9GPi8nQ0JJf36F4PD/lqbRc4WzDyFPAAAAAAAAAwgWbnOB7vP/4uf0xsvSsLW8r7S9w8jR3uLw7/T/foORBQoYAQAAAAAAAQACAQACAAAAAAAAT2mlpcD7wtj/y+H/yuD/wdf/psH/WnW1AAAAAAAAAQACAQACAAAAAAAAAAAEAwMIGSdGGylJLUR6NEuBNEuBLEN4GCZFGihIBQUKAAADAQAAAAAAAAABAAABAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAABAAABAAABAAABAAABAAABAQACAgADAQEFAQEFAQEFAQEFAgADAQACAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAABAAAB",
|
|
35
|
+
];
|
|
36
|
+
//# sourceMappingURL=brand-frames.js.map
|
package/dist/ui/brand.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brigade brand mark — animated coloured ASCII video clip beside a chunky wordmark.
|
|
3
|
+
*
|
|
4
|
+
* Layout (top to bottom):
|
|
5
|
+
*
|
|
6
|
+
* [pad]
|
|
7
|
+
* [ASCII video LEFT (56×30)] GAP [BRIGADE wordmark RIGHT (12 rows)]
|
|
8
|
+
* [tagline below the whole thing — cornsilk fullwidth]
|
|
9
|
+
* [pad]
|
|
10
|
+
*
|
|
11
|
+
* The video is a 56-col × 30-row × 89-frame char-based ASCII clip baked at
|
|
12
|
+
* module-build time (see scripts/gen-brand-frames-cli.mjs and brand-frames-cli.ts).
|
|
13
|
+
* Each cell carries a unicode codepoint and an RGB colour; we paint cells with
|
|
14
|
+
* `chalk.hex(toHex(r,g,b))(char)`, plus a skip-on-space-and-black optimisation
|
|
15
|
+
* that emits a literal space (no chalk wrapper) for empty/black cells so the
|
|
16
|
+
* pre-rendered string size stays sane while remaining visually identical.
|
|
17
|
+
* 89 frames at 15 fps play once, then the last frame holds.
|
|
18
|
+
*
|
|
19
|
+
* Wordmark colours are unchanged from the prior pure-text mark: a 4-stop
|
|
20
|
+
* vertical metallic gradient (cornsilk → gold → amber → bronze) so the
|
|
21
|
+
* lettering still reads as polished metal.
|
|
22
|
+
*/
|
|
23
|
+
// cfonts is CommonJS — Node's ESM loader can't statically extract named
|
|
24
|
+
// exports from it, so `import { render } from "cfonts"` fails at runtime
|
|
25
|
+
// even when esModuleInterop lets it typecheck. Use default-import + destructure.
|
|
26
|
+
import cfontsPkg from "cfonts";
|
|
27
|
+
const { render } = cfontsPkg;
|
|
28
|
+
import { Text } from "@mariozechner/pi-tui";
|
|
29
|
+
import chalk from "chalk";
|
|
30
|
+
import { VIDEO_COLS, VIDEO_FPS, VIDEO_FRAME_COUNT, VIDEO_FRAMES, VIDEO_ROWS } from "./brand-frames-cli.js";
|
|
31
|
+
// Wordmark palette — unchanged from the prior pure-text version.
|
|
32
|
+
const BRIGADE_HIGHLIGHT = "#FFF8DC"; // cornsilk — barely-there warm white
|
|
33
|
+
const BRIGADE_GOLD = "#FFD700"; // bright reflective gold
|
|
34
|
+
const BRIGADE_AMBER = "#FFBF00"; // amber midline
|
|
35
|
+
const BRIGADE_BRONZE = "#CD7F32"; // bronze fadeout
|
|
36
|
+
// Nearest-neighbour upscale factor applied to the cfonts `block` output.
|
|
37
|
+
// 2 = each char duplicated horizontally and each row duplicated vertically,
|
|
38
|
+
// so 6 letterform rows become 12. Tune this to scale the wordmark.
|
|
39
|
+
const WORDMARK_SCALE = 2;
|
|
40
|
+
// Cells of left margin applied to ALL rows of the wordmark column (wordmark
|
|
41
|
+
// glyphs, blank padding rows, and tagline). Shifts the entire left column
|
|
42
|
+
// right so it doesn't hug terminal column 0.
|
|
43
|
+
const WORDMARK_INDENT = 4;
|
|
44
|
+
// Cells of left margin applied to the tagline so it sits slightly inside
|
|
45
|
+
// the BRIGADE wordmark's left edge instead of flush with column 0.
|
|
46
|
+
const TAGLINE_INDENT = 10;
|
|
47
|
+
// Cells of vertical gap between the wordmark and the tagline.
|
|
48
|
+
const TAGLINE_TOP_GAP = 2;
|
|
49
|
+
// Per-row colour gradient distributed across the actual row count. With the
|
|
50
|
+
// 2x scale the wordmark is 12 rows; the 4-stop ramp flows smoothly regardless.
|
|
51
|
+
const COLOR_STOPS = [BRIGADE_HIGHLIGHT, BRIGADE_GOLD, BRIGADE_AMBER, BRIGADE_BRONZE];
|
|
52
|
+
function colorForRow(i, total) {
|
|
53
|
+
const t = total <= 1 ? 0 : i / (total - 1);
|
|
54
|
+
const idx = Math.min(COLOR_STOPS.length - 1, Math.floor(t * COLOR_STOPS.length));
|
|
55
|
+
return COLOR_STOPS[idx];
|
|
56
|
+
}
|
|
57
|
+
// Render the wordmark glyph shapes once at module load. cfonts is used only
|
|
58
|
+
// for SHAPES; we strip its colour escapes and re-apply our own per-row colour
|
|
59
|
+
// below so the gradient flows top-to-bottom (cfonts only does horizontal
|
|
60
|
+
// gradients).
|
|
61
|
+
const cfontsResult = render("BRIGADE", {
|
|
62
|
+
font: "block",
|
|
63
|
+
align: "left",
|
|
64
|
+
colors: ["white"],
|
|
65
|
+
background: "transparent",
|
|
66
|
+
letterSpacing: 1,
|
|
67
|
+
lineHeight: 1,
|
|
68
|
+
space: false,
|
|
69
|
+
maxLength: "0",
|
|
70
|
+
env: "node",
|
|
71
|
+
});
|
|
72
|
+
const RAW_ART = cfontsResult === false ? "BRIGADE" : cfontsResult.string;
|
|
73
|
+
// Strip every ANSI colour escape cfonts injected and split into rows. cfonts
|
|
74
|
+
// inserts blank padding rows even with `space: false`, which would shift the
|
|
75
|
+
// per-row colour mapping if we kept them. Then pixel-double (nearest-neighbour)
|
|
76
|
+
// each row by WORDMARK_SCALE in both axes — codepoint-aware via Array.from so
|
|
77
|
+
// multi-byte box-drawing glyphs ('╗','║','═','╔','╚','╝') aren't split between
|
|
78
|
+
// their UTF-16 surrogate halves.
|
|
79
|
+
const STRIPPED_ROWS = RAW_ART.replace(/\x1b\[[0-9;]*m/g, "")
|
|
80
|
+
.split("\n")
|
|
81
|
+
.filter((row) => row.trim().length > 0)
|
|
82
|
+
.flatMap((row) => {
|
|
83
|
+
const widened = Array.from(row)
|
|
84
|
+
.flatMap((ch) => Array.from({ length: WORDMARK_SCALE }, () => ch))
|
|
85
|
+
.join("");
|
|
86
|
+
return Array.from({ length: WORDMARK_SCALE }, () => widened);
|
|
87
|
+
});
|
|
88
|
+
const COLORED_WORDMARK_ROWS = STRIPPED_ROWS.map((row, idx) => chalk.hex(colorForRow(idx, STRIPPED_ROWS.length))(row));
|
|
89
|
+
// Convert ASCII letters/digits/printable punctuation to their Unicode
|
|
90
|
+
// FULLWIDTH twins (U+FFxx range, codepoint = ascii + 0xFEE0). Fullwidth
|
|
91
|
+
// glyphs render at 2 cells wide in most monospace fonts, which makes the
|
|
92
|
+
// tagline look visibly larger than plain ASCII without needing cfonts.
|
|
93
|
+
// SPACE becomes the ideographic space U+3000 so inter-word gaps stay 2 cells.
|
|
94
|
+
function toFullwidth(s) {
|
|
95
|
+
let out = "";
|
|
96
|
+
for (const ch of s) {
|
|
97
|
+
const cp = ch.codePointAt(0);
|
|
98
|
+
if (cp === undefined) {
|
|
99
|
+
out += ch;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (cp === 0x20) {
|
|
103
|
+
out += " ";
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (cp >= 0x21 && cp <= 0x7e) {
|
|
107
|
+
out += String.fromCodePoint(cp + 0xfee0);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
out += ch;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
const TAGLINE = chalk.hex(BRIGADE_HIGHLIGHT)(toFullwidth("your personal AI crew · by spinabot"));
|
|
115
|
+
// Three spaces between the video block and the wordmark. The video is now
|
|
116
|
+
// 56 cells wide (the char-based render) and the wordmark is ~146 cells once
|
|
117
|
+
// pixel-doubled, so the whole composite is ~205 cols wide and requires a
|
|
118
|
+
// wide terminal — that's expected for the brand splash.
|
|
119
|
+
const GAP = " ".repeat(25);
|
|
120
|
+
// Minimum terminal width (in cells) needed for the side-by-side layout to
|
|
121
|
+
// fit comfortably. The math:
|
|
122
|
+
// EFFECTIVE_WORDMARK_WIDTH (4 + 118 = 122)
|
|
123
|
+
// + GAP.length (25)
|
|
124
|
+
// + VIDEO_COLS (56)
|
|
125
|
+
// = 203 cells exactly. We add ~7 cells of breathing room so the user isn't
|
|
126
|
+
// staring at a layout pinned to the right edge of their terminal, giving
|
|
127
|
+
// THRESHOLD = 210. Below this width we stack the wordmark on top of the
|
|
128
|
+
// video instead of placing them side by side.
|
|
129
|
+
export const SIDE_BY_SIDE_THRESHOLD = 210;
|
|
130
|
+
// Wordmark column visible width is ~122 cells (WORDMARK_INDENT + WORDMARK_WIDTH);
|
|
131
|
+
// pad a few cells for safety so the wordmark never wraps into the gutter.
|
|
132
|
+
export const WORDMARK_MIN_WIDTH = 125;
|
|
133
|
+
// VIDEO_COLS exactly. Below this width the ASCII video can't fit either, so we
|
|
134
|
+
// render nothing at all rather than something visually broken.
|
|
135
|
+
export const ASCII_MIN_WIDTH = 56;
|
|
136
|
+
// Pick layout mode based on current terminal width. Pi-TUI's TUI exposes
|
|
137
|
+
// `tui.terminal.columns` (see node_modules/@mariozechner/pi-tui/dist/terminal.d.ts);
|
|
138
|
+
// we accept that as an optional override. Otherwise we fall back to
|
|
139
|
+
// process.stdout.columns. If neither is available (non-TTY pipe etc.) we
|
|
140
|
+
// assume the terminal is wide enough and keep the side-by-side layout —
|
|
141
|
+
// 999 is just a sentinel that comfortably exceeds THRESHOLD.
|
|
142
|
+
function pickLayout(termCols) {
|
|
143
|
+
const cols = termCols ?? process.stdout.columns ?? 999;
|
|
144
|
+
if (cols < ASCII_MIN_WIDTH)
|
|
145
|
+
return "empty";
|
|
146
|
+
if (cols < WORDMARK_MIN_WIDTH)
|
|
147
|
+
return "ascii-only";
|
|
148
|
+
if (cols < SIDE_BY_SIDE_THRESHOLD)
|
|
149
|
+
return "stack";
|
|
150
|
+
return "side";
|
|
151
|
+
}
|
|
152
|
+
// Blank rows prepended directly to the composed brand block so the wordmark
|
|
153
|
+
// doesn't hug the top of the terminal. Baked into the block content because
|
|
154
|
+
// pi-tui Text components measure their own height from non-empty content,
|
|
155
|
+
// so a leading "\n\n" Text padder doesn't reliably reserve visible rows.
|
|
156
|
+
const TOP_PAD_LINES = 8;
|
|
157
|
+
// Pre-render every video frame as an array of `VIDEO_ROWS` chalk-coloured
|
|
158
|
+
// strings. Each cell is a single Unicode glyph painted with `chalk.hex(rgb)`
|
|
159
|
+
// — one truecolor escape per cell. We skip the chalk wrapper for cells that
|
|
160
|
+
// are both a literal space AND fully black (invisible anyway), keeping the
|
|
161
|
+
// pre-rendered string size sane while remaining visually identical.
|
|
162
|
+
//
|
|
163
|
+
// Doing this once at module load means animation ticks are just an array
|
|
164
|
+
// lookup + setText — no per-tick chalk allocation.
|
|
165
|
+
//
|
|
166
|
+
// Iteration is codepoint-aware via Array.from(chars) so multi-byte glyphs
|
|
167
|
+
// (Braille, box-drawing, etc.) aren't split between their UTF-16 surrogate
|
|
168
|
+
// halves. Source frames carry exactly VIDEO_COLS * VIDEO_ROWS codepoints and
|
|
169
|
+
// VIDEO_COLS * VIDEO_ROWS * 3 RGB bytes (validated at generation time).
|
|
170
|
+
const PRE_RENDERED_FRAMES = VIDEO_FRAMES.map(({ chars, rgb }) => {
|
|
171
|
+
const buf = Buffer.from(rgb, "base64");
|
|
172
|
+
const cps = Array.from(chars);
|
|
173
|
+
const rows = [];
|
|
174
|
+
for (let y = 0; y < VIDEO_ROWS; y++) {
|
|
175
|
+
let line = "";
|
|
176
|
+
for (let x = 0; x < VIDEO_COLS; x++) {
|
|
177
|
+
const i = y * VIDEO_COLS + x;
|
|
178
|
+
const ch = cps[i] ?? " ";
|
|
179
|
+
const r = buf[i * 3] ?? 0;
|
|
180
|
+
const g = buf[i * 3 + 1] ?? 0;
|
|
181
|
+
const b = buf[i * 3 + 2] ?? 0;
|
|
182
|
+
if (ch === " " && r === 0 && g === 0 && b === 0) {
|
|
183
|
+
line += " ";
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const hex = `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
187
|
+
line += chalk.hex(hex)(ch);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
rows.push(line);
|
|
191
|
+
}
|
|
192
|
+
return rows;
|
|
193
|
+
});
|
|
194
|
+
// Visible width of an arbitrary string after stripping ANSI colour escapes.
|
|
195
|
+
// Used to right-pad the tagline row so its leftColumn entry matches the
|
|
196
|
+
// wordmark rows in width, keeping the ASCII video aligned on every row.
|
|
197
|
+
//
|
|
198
|
+
// Cell-aware: fullwidth / wide characters (e.g. those produced by toFullwidth,
|
|
199
|
+
// CJK ideographs, the ideographic space U+3000) render as TWO terminal cells
|
|
200
|
+
// each but count as ONE codepoint in JavaScript. Counting visual cells — not
|
|
201
|
+
// codepoints — keeps the tagline row's width matched to the wordmark rows so
|
|
202
|
+
// the ASCII video doesn't shift right on the tagline row.
|
|
203
|
+
function visibleWidth(s) {
|
|
204
|
+
// Strip ANSI escapes first.
|
|
205
|
+
const stripped = s.replace(/\[[0-9;]*m/g, "");
|
|
206
|
+
// Count visual cells: fullwidth / wide characters take 2 cells, the rest 1.
|
|
207
|
+
// We approximate "wide" via the common East Asian Width / Halfwidth-Fullwidth
|
|
208
|
+
// ranges that toFullwidth produces, plus CJK ideographs and the
|
|
209
|
+
// ideographic space U+3000 used for fullwidth spaces.
|
|
210
|
+
let cells = 0;
|
|
211
|
+
for (const ch of stripped) {
|
|
212
|
+
const cp = ch.codePointAt(0);
|
|
213
|
+
if (cp === undefined)
|
|
214
|
+
continue;
|
|
215
|
+
const wide = cp === 0x3000 || // ideographic space
|
|
216
|
+
(cp >= 0xff01 && cp <= 0xff60) || // fullwidth ASCII variants
|
|
217
|
+
(cp >= 0xffe0 && cp <= 0xffe6) || // fullwidth signs
|
|
218
|
+
(cp >= 0x1100 && cp <= 0x115f) || // Hangul Jamo
|
|
219
|
+
(cp >= 0x2e80 && cp <= 0x303e) || // CJK Radicals/Symbols
|
|
220
|
+
(cp >= 0x3041 && cp <= 0x33ff) || // Kana, Bopomofo, CJK Compat
|
|
221
|
+
(cp >= 0x3400 && cp <= 0x4dbf) || // CJK Ext A
|
|
222
|
+
(cp >= 0x4e00 && cp <= 0x9fff) || // CJK Unified
|
|
223
|
+
(cp >= 0xa000 && cp <= 0xa4cf) || // Yi
|
|
224
|
+
(cp >= 0xac00 && cp <= 0xd7a3); // Hangul Syllables
|
|
225
|
+
cells += wide ? 2 : 1;
|
|
226
|
+
}
|
|
227
|
+
return cells;
|
|
228
|
+
}
|
|
229
|
+
// Cell-aware visible width of one wordmark row. Used to pad the left column
|
|
230
|
+
// with blank space once the wordmark runs out so the ASCII video on the right
|
|
231
|
+
// doesn't shift left in those rows. The current cfonts `block` output is all
|
|
232
|
+
// ASCII + box-drawing (1-cell glyphs), so the value here matches raw .length —
|
|
233
|
+
// using visibleWidth() just adds robustness if the wordmark ever changes.
|
|
234
|
+
const WORDMARK_WIDTH = COLORED_WORDMARK_ROWS[0] ? visibleWidth(COLORED_WORDMARK_ROWS[0]) : 0;
|
|
235
|
+
// Build the LEFT COLUMN once at module load: wordmark rows, then a vertical
|
|
236
|
+
// gap, then the indented tagline. This mirrors the OpenTUI layout in
|
|
237
|
+
// asciify-engine/tui-demo/brand.tsx where the Wordmark component is a column
|
|
238
|
+
// of [wordmark rows][marginTop gap][indented tagline] sitting beside the
|
|
239
|
+
// ASCII video. Total = 12 wordmark rows + 2 blank rows + 1 tagline row = 15.
|
|
240
|
+
// Every row in leftColumn must be padded to EFFECTIVE_WORDMARK_WIDTH visible
|
|
241
|
+
// cells so composeFrame's `leftPart + GAP + videoRow` keeps the video at the
|
|
242
|
+
// same screen column on every row (otherwise the tagline row, whose visible
|
|
243
|
+
// width is less than the wordmark width, would let the video shift left
|
|
244
|
+
// there). Each row gets WORDMARK_INDENT spaces prepended to shift the whole
|
|
245
|
+
// left column right by that many cells; the ASCII video on the right shifts
|
|
246
|
+
// in lockstep because every row carries the same prefix.
|
|
247
|
+
// Math.max(0, …) means we never truncate if the tagline ever happens to
|
|
248
|
+
// exceed EFFECTIVE_WORDMARK_WIDTH — we just skip the padding in that case.
|
|
249
|
+
const EFFECTIVE_WORDMARK_WIDTH = WORDMARK_INDENT + WORDMARK_WIDTH;
|
|
250
|
+
const taglineRow = `${" ".repeat(WORDMARK_INDENT)}${" ".repeat(TAGLINE_INDENT)}${TAGLINE}`;
|
|
251
|
+
const taglineVisibleLen = visibleWidth(taglineRow);
|
|
252
|
+
const taglinePadding = Math.max(0, EFFECTIVE_WORDMARK_WIDTH - taglineVisibleLen);
|
|
253
|
+
const leftColumn = [
|
|
254
|
+
...Array.from({ length: TOP_PAD_LINES }, () => " ".repeat(EFFECTIVE_WORDMARK_WIDTH)),
|
|
255
|
+
...COLORED_WORDMARK_ROWS.map((row) => `${" ".repeat(WORDMARK_INDENT)}${row}`),
|
|
256
|
+
...Array(TAGLINE_TOP_GAP).fill(" ".repeat(EFFECTIVE_WORDMARK_WIDTH)),
|
|
257
|
+
`${taglineRow}${" ".repeat(taglinePadding)}`,
|
|
258
|
+
];
|
|
259
|
+
// Compose one complete brand block (wordmark || video) for a given frame
|
|
260
|
+
// index. Returns the multiline string ready to drop into a single Text
|
|
261
|
+
// component — keeping it a single component lets Pi-TUI redraw it atomically
|
|
262
|
+
// with no row-tearing during animation.
|
|
263
|
+
//
|
|
264
|
+
// Layout adapts to terminal width via pickLayout():
|
|
265
|
+
// - "side": wordmark column LEFT + GAP + video rows RIGHT, paired row-by-row.
|
|
266
|
+
// - "stack": wordmark column on top, blank separator row, then video rows
|
|
267
|
+
// indented with WORDMARK_INDENT so the video lines up flush-left
|
|
268
|
+
// with the wordmark column instead of hugging column 0.
|
|
269
|
+
function composeFrame(frameIdx, termCols) {
|
|
270
|
+
const videoRows = PRE_RENDERED_FRAMES[frameIdx] ?? PRE_RENDERED_FRAMES[0] ?? [];
|
|
271
|
+
const mode = pickLayout(termCols);
|
|
272
|
+
if (mode === "empty") {
|
|
273
|
+
return "";
|
|
274
|
+
}
|
|
275
|
+
if (mode === "ascii-only") {
|
|
276
|
+
const videoIndent = " ".repeat(WORDMARK_INDENT);
|
|
277
|
+
const lines = [];
|
|
278
|
+
for (let i = 0; i < TOP_PAD_LINES; i++)
|
|
279
|
+
lines.push("");
|
|
280
|
+
for (const row of videoRows)
|
|
281
|
+
lines.push(`${videoIndent}${row}`);
|
|
282
|
+
return lines.join("\n");
|
|
283
|
+
}
|
|
284
|
+
if (mode === "stack") {
|
|
285
|
+
const videoIndent = " ".repeat(WORDMARK_INDENT);
|
|
286
|
+
const lines = [];
|
|
287
|
+
for (const row of leftColumn)
|
|
288
|
+
lines.push(row);
|
|
289
|
+
lines.push("");
|
|
290
|
+
for (const row of videoRows)
|
|
291
|
+
lines.push(`${videoIndent}${row}`);
|
|
292
|
+
return lines.join("\n");
|
|
293
|
+
}
|
|
294
|
+
const totalRows = Math.max(leftColumn.length, VIDEO_ROWS);
|
|
295
|
+
const wordmarkBlankPad = " ".repeat(EFFECTIVE_WORDMARK_WIDTH);
|
|
296
|
+
const videoBlankPad = " ".repeat(VIDEO_COLS);
|
|
297
|
+
const lines = [];
|
|
298
|
+
for (let r = 0; r < totalRows; r++) {
|
|
299
|
+
const leftPart = leftColumn[r] ?? wordmarkBlankPad;
|
|
300
|
+
const rightPart = videoRows[r] ?? videoBlankPad;
|
|
301
|
+
lines.push(`${leftPart}${GAP}${rightPart}`);
|
|
302
|
+
}
|
|
303
|
+
return lines.join("\n");
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Render the wordmark + animated video clip + tagline. Spawns a setInterval
|
|
307
|
+
* that drives the animation; the frame index wraps via modulo so the clip
|
|
308
|
+
* loops continuously for the rest of the session.
|
|
309
|
+
*
|
|
310
|
+
* Returns the components added to the TUI (caller can use these to remove or
|
|
311
|
+
* inspect them).
|
|
312
|
+
*/
|
|
313
|
+
export function renderBrandHeader(tui) {
|
|
314
|
+
const components = [];
|
|
315
|
+
// Pi-TUI's TUI exposes the underlying Terminal which has a `columns` getter
|
|
316
|
+
// (see node_modules/@mariozechner/pi-tui/dist/terminal.d.ts). Prefer that
|
|
317
|
+
// over process.stdout.columns since it's the value pi-tui itself uses for
|
|
318
|
+
// rendering, but pickLayout falls back if either is unavailable.
|
|
319
|
+
const getCols = () => tui.terminal?.columns ?? process.stdout.columns;
|
|
320
|
+
const padTop = new Text("\n\n", 0, 0);
|
|
321
|
+
tui.addChild(padTop);
|
|
322
|
+
components.push(padTop);
|
|
323
|
+
const block = new Text(composeFrame(0, getCols()), 0, 0);
|
|
324
|
+
tui.addChild(block);
|
|
325
|
+
components.push(block);
|
|
326
|
+
const padBottom = new Text("", 0, 0);
|
|
327
|
+
tui.addChild(padBottom);
|
|
328
|
+
components.push(padBottom);
|
|
329
|
+
tui.requestRender();
|
|
330
|
+
// --- Scrollback-friendly render scheduling ---------------------------------
|
|
331
|
+
// pi-tui's differential renderer (see node_modules/@mariozechner/pi-tui/dist/tui.js
|
|
332
|
+
// `doRender`, lines ~674-971) repaints animation frames by moving the
|
|
333
|
+
// hardware cursor up with `\x1b[{n}A`, clearing each row with `\x1b[2K`, and
|
|
334
|
+
// writing the new content. Windows Terminal (and other Conhost-derived
|
|
335
|
+
// emulators) treat any write that targets the bottom region of the buffer
|
|
336
|
+
// as "active output" and snap the viewport to the cursor — fighting the
|
|
337
|
+
// user's scrollback. Two complementary mitigations:
|
|
338
|
+
//
|
|
339
|
+
// 1. Skip the call entirely when the composed frame is byte-identical to
|
|
340
|
+
// what we already pushed (e.g. when terminal width hasn't changed and
|
|
341
|
+
// the layout produced the same string for an empty/clamped layout).
|
|
342
|
+
// This avoids a no-op render that still emits cursor-move bytes.
|
|
343
|
+
//
|
|
344
|
+
// 2. Wrap each tui.requestRender() in DEC mode 2026 (synchronized output
|
|
345
|
+
// begin/end) AND VT100 save/restore-cursor (`\x1b[s` / `\x1b[u`). pi-tui
|
|
346
|
+
// already opens 2026 inside doRender, but it does so AROUND its cursor
|
|
347
|
+
// moves; wrapping at the outer scope means Windows Terminal sees the
|
|
348
|
+
// full repaint as one atomic frame and won't auto-scroll the viewport
|
|
349
|
+
// to follow intermediate cursor moves. The save/restore pair is a
|
|
350
|
+
// belt-and-braces signal that the terminal can keep its viewport pinned
|
|
351
|
+
// where the user put it. Both sequences are cheap (≤8 bytes each) and
|
|
352
|
+
// are no-ops on terminals that don't honour them.
|
|
353
|
+
//
|
|
354
|
+
// References:
|
|
355
|
+
// - pi-tui differential renderer: node_modules/@mariozechner/pi-tui/dist/tui.js:674-971
|
|
356
|
+
// - Synchronized Output (DEC 2026): https://gitlab.com/gnachman/iterm2/-/wikis/synchronized-updates-spec
|
|
357
|
+
// - Cursor save/restore (DECSC / DECRC, ANSI \x1b[s/\x1b[u): VT100 spec
|
|
358
|
+
const SYNC_BEGIN = "\x1b[?2026h\x1b[s"; // begin sync + save cursor
|
|
359
|
+
const SYNC_END = "\x1b[u\x1b[?2026l"; // restore cursor + end sync
|
|
360
|
+
let lastFrameStr;
|
|
361
|
+
const safeRender = (next) => {
|
|
362
|
+
if (next === lastFrameStr) {
|
|
363
|
+
// No visible change — don't re-emit cursor-move bytes that would
|
|
364
|
+
// make the terminal re-anchor the viewport.
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
lastFrameStr = next;
|
|
368
|
+
block.setText(next);
|
|
369
|
+
try {
|
|
370
|
+
process.stdout.write(SYNC_BEGIN);
|
|
371
|
+
tui.requestRender();
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
process.stdout.write(SYNC_END);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
// Drive the animation. The interval fires every 1000/VIDEO_FPS ms and the
|
|
378
|
+
// frame index wraps via modulo so the clip loops continuously for the rest
|
|
379
|
+
// of the session — no early-exit clearInterval. We refetch the terminal
|
|
380
|
+
// width on every tick so resizes are reflected within ~1/VIDEO_FPS s
|
|
381
|
+
// even if the dedicated resize listener below misses for some reason.
|
|
382
|
+
let frameIdx = 0;
|
|
383
|
+
const tickMs = Math.max(1, Math.round(1000 / VIDEO_FPS));
|
|
384
|
+
const timer = setInterval(() => {
|
|
385
|
+
frameIdx = (frameIdx + 1) % VIDEO_FRAME_COUNT;
|
|
386
|
+
safeRender(composeFrame(frameIdx, getCols()));
|
|
387
|
+
}, tickMs);
|
|
388
|
+
// Don't keep the event loop alive just for this animation — once the user
|
|
389
|
+
// quits, the process should be free to exit even if the timer hasn't yet
|
|
390
|
+
// reached the final frame.
|
|
391
|
+
timer.unref?.();
|
|
392
|
+
// Re-compose the brand block on terminal resize so the responsive layout
|
|
393
|
+
// (side / stack / ascii-only / empty) keeps working as the animation
|
|
394
|
+
// loops forever. During the animation the safeRender identical-frame
|
|
395
|
+
// cache makes this a no-op when the layout didn't actually change.
|
|
396
|
+
const onResize = () => {
|
|
397
|
+
safeRender(composeFrame(frameIdx, getCols()));
|
|
398
|
+
};
|
|
399
|
+
process.stdout.on("resize", onResize);
|
|
400
|
+
return components;
|
|
401
|
+
}
|
|
402
|
+
//# sourceMappingURL=brand.js.map
|